← 返回 Game Development / Unity / MagicGameHarness

2026-08-21 ·30 min

Magic Game Harness vs Bevy ECS

这不是 spec 实现笔记,是对比分析笔记——通过 “Rust 生态最成功的 ECS 游戏引擎” 的设计来理解 magic-game-harness-unity 的架构选择。

Bevy = Rust 编写的现代化 ECS(Entity-Component-System)游戏引擎,2020 年至今。 本篇代码以 Bevy 0.15 为准。 Bevy 仍处在 0.x 阶段(不存在 5.x 这样的版本号),且每个 minor 都可能 breaking——引用本篇代码前请先核对自己的 Bevy 版本。已知的一处变动:0.16 起 EventWriter::send 更名为 write,本篇 §3.3 的例子用的是 0.15 的 send。 Bevy 是 Rust 游戏开发生态里最活跃、社区规模最大的引擎。


0. 为什么对比 Bevy?

Bevy 与 magic-game-harness-unity 完全不共享技术栈(Rust vs C#,原生 vs Unity),但设计哲学惊人相似:

Bevy 概念magic-game-harness-unity 对应相似度
PluginModule (IModule)⭐⭐⭐⭐⭐
ResourceBootstrap 注册的服务⭐⭐⭐⭐
ComponentModule-owned 数据⭐⭐⭐
SystemMod 中的事件处理器⭐⭐⭐
WorldSession⭐⭐⭐⭐
AppApp LifetimeScope⭐⭐⭐⭐⭐
ScheduleLifecycle 状态机⭐⭐⭐
EventDiagnosticEvent⭐⭐⭐
SystemParamIModuleContext⭐⭐⭐⭐

两个系统的目的都是**:让游戏开发者组合独立功能,而不是继承一个大类。


1. Bevy 快速回顾

1.1 一个最小的 Bevy 程序

use bevy::prelude::*;
fn main() {
App::new()
.add_plugins(DefaultPlugins) // ← 类似 AppLifetimeScope 装配
.add_systems(Startup, setup) // ← 类似 SessionFactory.CreateSession
.add_systems(Update, move_player) // ← 类似 IModule.Activate
.run();
}
fn setup(mut commands: Commands) {
commands.spawn(Player); // ← 类似 fiber 创建
}
fn move_player(mut query: Query<&mut Transform, With<Player>>, ...) { ... }
// ^^^ 要可变借出 Transform,query 本身必须是 mut

1.2 Bevy 的核心机制

机制解释
ECS(Entity-Component-System)Entity = 编号,Component = 数据,System = 函数
Plugin一个 trait,把自己的 ECS 内容注册到 App
Resource全局单例数据(不是 Entity 上的 Component)
Schedule一组按阶段执行的 System
World一个 ECS 实例(可以多个)
App顶层运行时容器

2. 概念对照表

概念Bevymagic-game-harness-unity
App 入口App::new()AppLifetimeScope.prefab (Unity Inspector 配置)
插件/mod 入口impl Plugin { fn build(&self, app: &mut App) {...} }impl IModule { fn ActivateAsync(...) }
生命周期阶段Plugin::build → ready → finish → cleanup(Plugin trait 的四个钩子)Created → Starting → Running → Stopping → Stopped/Failed
资源注册app.insert_resource(MyResource)builder.RegisterInstance<T>(instance)
资源读取fn my_system(my_resource: Res<MyResource>) {...}VContainer constructor injection
类型系统Rust trait(强类型)C# interface(强类型)
动态加载❌ 编译时✅ HybridCLR(运行时)
按数据筛选实体Query<&T, With<U>>—(harness 不是 ECS,没有对应概念)
取一个全局服务Res<T> / ResMut<T>VContainer 构造函数注入(框架内部);Mod 侧未来是 TryGetCapability(key, version, out object)(非泛型,见下方 ⚠️)
无约束地拿任何东西&mut World刻意不提供——Module_context_is_not_an_unrestricted_service_locator 明确禁止
事件总线EventWriter<MyEvent> / EventReader<MyEvent>router.Emit(diagnostic) + 未来的 event bus
多实例可同时持有多个 World同一时刻至多一个 active Session(SessionFactory.CreateSession 里 if (activeSession != null) throw),多个 Session 是顺序复用而非并存

⚠️ 原表里”服务定位:Query<T> ↔ ctx.GetCapability<T>(key)”这一行有两个错,已拆成上面四行:

  1. Bevy 侧:Query<T> 是按组件筛选实体的接口,不是服务定位。取全局服务的是 Res<T> / ResMut<T>,无约束访问的是 &mut World。

  2. harness 侧:ctx.GetCapability<T>(key) 是泛型方法,与被测试钉死的架构约束直接冲突:

    // PublicApiSurfaceTests.cs:62-68
    Assert.That(methods.Any(m => m.IsGenericMethod), Is.False); // IModuleContext 上禁止一切泛型方法

    未来的能力查询 API 只能是非泛型形态(bool TryGetCapability(CapabilityKey, CapabilityVersion, out object)),类型安全由单独打包的契约程序集(spec §8.5)承担。理由见 06-Mod-Distribution.md §1.5:capability 的身份必须能脱离运行时、以数据形式写进 manifest 和 lockfile,而泛型参数 T 只活在编译期。

第 3 行那个”刻意不提供 &mut World 等价物”其实是本篇最值得展开的对比点——Bevy 敢给 &mut World,是因为它的插件全部编译期静态链接、作者与引擎版本强绑定;harness 不敢给,是因为它的模块是运行时加载的第三方二进制。同样的能力,在”可信代码”和”半可信代码”两种前提下答案完全相反。


📌 全篇代码约定

本篇后续(§3.2、§3.5、§4.1、§5.1、§10 等)多处出现 ctx.RegisterCapability<IGameRules>(...) / ctx.GetCapability<IGameRules>("...") 这样的写法。这是为了对比行文方便而使用的占位伪代码,当前代码库中不存在,且泛型形态与上面那条架构测试冲突。

阅读时请一律按下面这个形态理解:

// 本篇伪代码: ctx.RegisterCapability<IGameRules>(new AlphaRules())
// 实际只可能长成(返回 IDisposable 句柄,对应 spec §6 原则 5 的 LIFO 回收):
IDisposable handle = ctx.RegisterCapability(
CapabilityKey.Parse("game.alpha-rules"),
CapabilityVersion.Parse("1.0.0"),
new AlphaRules());
// 本篇伪代码: ctx.GetCapability<IGameRules>("game.alpha-rules")
// 实际只可能长成:
if (ctx.TryGetCapability(CapabilityKey.Parse("game.alpha-rules"),
CapabilityVersion.Parse("1.0.0"),
out var provider))
{
var rules = (IGameRules)provider; // IGameRules 来自独立打包的契约程序集
}

这个差别不只是语法。 泛型版本把”契约身份”绑在 C# 类型上,非泛型版本把它绑在 CapabilityKey 字符串上——后者才能被写进 manifest、被 lockfile 冻结、被安装器在下载前校验。本篇 §4.1 讨论”Data-Oriented vs Contract-Oriented”时,这正是 Contract-Oriented 一侧付出的代价与换来的收益。 | 调度器 | Schedule (Startup/Update/FixedUpdate/…) | Lifecycle state machine |


3. 关键设计决策对比

3.1 决策 A:插件/mod 的结构

Bevy

// Bevy Plugin 风格
pub struct MyPlugin;
impl Plugin for MyPlugin {
fn build(&self, app: &mut App) {
app.insert_resource(MyResource { ... });
app.add_systems(Startup, my_system);
app.add_event::<MyEvent>();
}
// 可选:ready 钩子
fn ready(&self, app: &mut App, ... ) {
// App 完全装配完后调用
}
}

特点:

  • ✅ build 在 App 装配时调用(编译时)
  • ✅ ready 在所有 plugin 都 build 完后调用
  • ✅ 没有”激活”概念——plugin 装配即生效

magic-game-harness-unity

// magic-game-harness-unity Module 风格
public sealed class AlphaModule : IModule
{
public ModuleDescriptor Descriptor { get; } = new ModuleDescriptor(...);
public async Task<ModuleOperationResult> ActivateAsync(IModuleContext ctx, CancellationToken ct)
{
ctx.RegisterCapability<IGameRules>(new AlphaRules());
return ModuleOperationResult.Success();
}
public Task<ModuleOperationResult> DeactivateAsync(CancellationToken ct)
{
return Task.FromResult(ModuleOperationResult.Success());
}
}

特点:

  • ✅ ActivateAsync 在运行时被调用(HybridCLR 加载 dll 后)
  • ✅ 可以被禁用、卸载、替换
  • ✅ 通过 capability 显式声明”我能提供什么”

核心区别:

  • Bevy plugin 是编译时的(不可热更)
  • magic-game-harness-unity module 是运行时的(可热更、可禁用)

这是各自生态的最优解:

  • Bevy 选 Rust → 编译时一切最优
  • magic-game-harness-unity 选 Unity + HybridCLR → 运行时更新可能

3.2 决策 B:资源/服务注册

Bevy

// 直接插入资源
app.insert_resource(MyResource { count: 0 });
// 在 system 中读取
fn increment(mut counter: ResMut<MyResource>) {
counter.count += 1;
}

特点:

  • ✅ 通过类型直接查找(不需要 key)
  • ✅ Res / ResMut 显式标注只读/可写
  • ✅ 编译期保证类型正确

magic-game-harness-unity

// 通过 VContainer 注入(编译时类型)
builder.RegisterInstance<IGameRules>(rules);
// 通过 capability key + version(运行时)
ctx.GetCapability<IGameRules>("game.alpha-rules");

特点:

  • ✅ VContainer 走编译时类型(强类型)
  • ✅ Capability 走运行时 key(可热更、可替换)

核心区别:

  • Bevy 是完全编译时的类型系统
  • magic-game-harness-unity 是编译时 + 运行时双轨

为什么 magic-game-harness-unity 需要 key?

因为 mod 是运行时加载——编译期不知道某个 capability 由谁提供。key + version 是”运行时合约”。

3.3 决策 C:事件 / 消息

Bevy

// 1. 定义事件
#[derive(Event)]
struct CollisionEvent(Entity, Entity);
// 2. 注册事件
app.add_event::<CollisionEvent>();
// 3. 发送
fn detect_collisions(mut events: EventWriter<CollisionEvent>) {
events.send(CollisionEvent(e1, e2));
}
// 4. 接收
fn handle_collisions(mut events: EventReader<CollisionEvent>) {
for CollisionEvent(e1, e2) in events.read() {
// ...
}
}

特点:

  • ✅ 类型安全(事件是 struct)
  • ✅ 编译期保证发送/接收类型一致
  • ❌ 全局——任何 system 都能发送/接收任何事件

magic-game-harness-unity

当前实现(DiagnosticEvent):

// 1. 框架定义事件
public sealed class DiagnosticEvent
{
public DateTimeOffset Timestamp { get; }
public DiagnosticSeverity Severity { get; }
public DiagnosticEventName Name { get; }
// ...
}
// 2. Router 路由
router.Emit(diagnosticEvent);
// 3. 多个 sink 接收
public class UnityConsoleDiagnosticSink : IDiagnosticSink
{
public void Write(DiagnosticEvent evt) { /* Debug.Log */ }
}

特点:

  • ✅ 类型安全
  • ✅ 多个 sink 可以独立订阅
  • ❌ 当前只有诊断用事件系统——mod 自己的事件总线未实现

未来 mod 事件总线(推断):

// 推测的实现
app.RegisterEvent<MyModEvent>();
// 发送
ctx.Events.Send(new MyModEvent(...));
// 接收
app.AddSystem(SystemStage.Update, ctx => {
foreach (var evt in ctx.Events.Read<MyModEvent>()) {
// ...
}
});

与 Bevy 的对比:

  • Bevy 事件是全局的——所有 system 都能读
  • magic-game-harness-unity 推测会按 module scope——mod 只能订阅自己 registered 的事件

3.4 决策 D:状态机

Bevy

// Bevy state 风格
#[derive(States, Default, Debug, Clone, PartialEq, Eq, Hash)]
enum GameState {
#[default]
Menu,
Playing,
Paused,
}
app.init_state::<GameState>()
.add_systems(OnEnter(GameState::Playing), start_game)
.add_systems(OnExit(GameState::Playing), end_game);

特点:

  • ✅ 全局状态机(一个 state)
  • ✅ 转换事件自动触发(OnEnter / OnExit)
  • ❌ 没有”进入/退出失败”语义

magic-game-harness-unity

// Session 状态机
public enum SessionState
{
Created = 0, Starting = 1, Running = 2,
Stopping = 3, Stopped = 4, Failed = 5,
}

特点:

  • ✅ 状态机有失败终态(Failed)
  • ✅ 终态不可逆(Stopped/Failed 不能 Restart)
  • ✅ 每个状态切换有确定性事件
  • ✅ memoized terminal Task(spec 第 10 节)

与 Bevy 的对比:

维度Bevymagic-game-harness-unity
状态粒度全局单 state每 Session 一个 state
失败语义❌ 没有显式 Failed✅ Failed 终态
可逆性任意转换终态不可逆
转换代价OnEnter/OnExit 触发 systemTask 序列化保证 + 清理 in finally

magic-game-harness-unity 更严格——因为mod 加载是 critical path,出错必须立刻知道并清理。

3.5 决策 E:依赖注入 / 服务定位

Bevy

// Bevy 通过 SystemParam 注入
fn my_system(
time: Res<Time>, // 全局资源
query: Query<&Transform, With<Player>>, // Entity 查询
mut events: EventWriter<MyEvent>, // 事件写入器
) {
// ...
}

特点:

  • ✅ System 自动接收需要的参数
  • ✅ 编译期保证参数类型存在
  • ❌ 全局服务注册(无 namespace)

magic-game-harness-unity

// 通过 VContainer 构造函数注入
public class MyService
{
public MyService(
IGameRules rules, // 编译期注入
ISessionLifetime lifetime) // 编译期注入
{ ... }
}
// 通过 capability key 查找
var rules = ctx.GetCapability<IGameRules>("game.alpha-rules");

特点:

  • ✅ VContainer 是编译期类型注入(构造器)
  • ✅ Capability 是运行时 key + version(可替换)
  • ✅ Session scope 隔离(lifetime 在 Session 销毁时失效)

对比:magic-game-harness-unity 把”硬依赖”和”软依赖”分开:

  • 硬依赖(构造器参数):编译时知道是谁提供的
  • 软依赖(capability):运行时协商

这是 Bevy 没有的设计——Bevy 没有”mod 替换”概念,所以也不需要软依赖。


4. 关键设计哲学对比

4.1 哲学 #1:Data-Oriented vs Contract-Oriented

Bevy

Data-Oriented:把数据放在 Component 里,把逻辑放在 System 里。数据驱动一切。

// 玩家 = 一堆 Component
commands.spawn((
Player, // 标记 Component
Transform::default(), // 数据
Health(100), // 数据
Speed(5.0), // 数据
));
// System 操作数据
fn move_player(mut q: Query<(&mut Transform, &Speed), With<Player>>, time: Res<Time>) {
for (mut t, s) in &mut q {
t.translation += Vec3::new(s.0 * time.delta_seconds(), 0.0, 0.0);
}
}

magic-game-harness-unity

Contract-Oriented:把能力封装在 Contract interface 里,通过 capability 声明和提供。

// 能力 = contract 接口
public interface IGameRules
{
void SetBlock(int x, int y, int z, Block block);
Block GetBlock(int x, int y, int z);
}
// Mod 提供能力
ctx.RegisterCapability<IGameRules>(new AlphaRules());
// Mod 消费能力
var rules = ctx.GetCapability<IGameRules>("game.alpha-rules");
rules.SetBlock(0, 0, 0, new Block());

对比:

维度Bevy(数据驱动)magic-game-harness-unity(合约驱动)
核心单元Component(数据)Capability(合约)
Logic 放在哪System 函数Contract 实现类
数据组织Archetype(同一 Component 组合放一起)不关心(Contract 自己管)
性能优化Cache-friendly(数据连续)虚调用(契约调用)
mod 替换困难(Component 改了所有 System 都要改)容易(换 Capability 实现即可)

取舍:

  • Bevy 选性能——data-oriented 适合 cache locality
  • magic-game-harness-unity 选灵活性——合约驱动适合 mod 替换

4.2 哲学 #2:Hot Reload

Bevy

❌ 不支持 hot reload——Rust 编译模型 + ECS 数据布局,热更几乎不可能。

magic-game-harness-unity

✅ 支持 hot reload——HybridCLR + Logical Unload + Capability Replacement。

为什么 HybridCLR 能做,Bevy 不能?

  • HybridCLR 是保留 JIT 能力的受限 IL2CPP——可以加载额外 dll
  • Rust 编译后是 native code——没有 JIT,没法加载额外代码
  • Unity 的 Addressables + Resources 系统支持动态加载
  • Bevy 必须用 dynamic library(.so / .dll)+ 复杂 ABI——非常痛苦

4.3 哲学 #3:Type System Strictness

Bevy

Rust 的强类型系统——任何类型不匹配都是编译错误。

// 编译错误:MyResource 没有实现 Default
fn setup(mut commands: Commands) {
commands.insert_resource(MyResource); // ❌ 编译错
}

magic-game-harness-unity

C# 的强类型 + 运行时验证——类型不匹配编译通过但运行时验证。

// 编译通过
var version = SemanticVersion.Parse("invalid"); // ← 抛 FormatException

对比:Bevy 把所有错误推到编译期(开发期发现);magic-game-harness-unity 部分错误留到运行时(用户使用期发现)。

取舍:

  • Bevy 开发体验更好——错误早发现
  • magic-game-harness-unity 运行时更灵活——可以处理配置错误(用户的 manifest.json 写错)

4.4 哲学 #4:Performance vs Flexibility

Bevy 的取舍

  • ✅ 极致性能——cache-friendly、SIMD 友好、native code
  • ❌ 不灵活——重编译慢、热更难、类型系统死板

magic-game-harness-unity 的取舍

  • ✅ 极致灵活——mod 可替换、capability 协商、运行时升级
  • ❌ 性能开销——虚调用、capability lookup、IL2CPP 反射

关键问题:magic-game-harness-unity 的”灵活性”具体牺牲了多少性能?

操作Bevymagic-game-harness-unity倍率
跨 mod 调用不支持虚调用n/a
单数据访问直接内存Dictionary lookup + 虚调用~10-100x
热更新重启HybridCLR load~100x faster restart
多 mod 加载重编译几百 ms远快

结论:magic-game-harness-unity 的单次调用比 Bevy 慢,但迭代速度远快。这是 Unity / C# 生态的典型 trade-off。


5. Bevy 可以向 magic-game-harness-unity 借鉴什么

5.1 借鉴 #1:显式 Capability 而不是 Resource

Bevy 的 Res<T> 是全局单例——任何 system 都能拿到。问题:

  • ❌ 多个 mod 想要”同一种 Resource” 会冲突
  • ❌ 无法表达”这个 Resource 由某个 mod 提供”
  • ❌ 无法表达”我的 system 依赖某个 Resource 必须存在”

magic-game-harness-unity 的 capability 系统:

// Mod A 提供
ctx.RegisterCapability<IGameRules>(new AlphaRules());
// Mod B 消费
public class ConsumerMod : IModule
{
public ModuleDescriptor Descriptor => new ModuleDescriptor(
...,
requiredCapabilities: new[]
{
new CapabilityRequirement("game.alpha-rules", CapabilityVersion.Parse("1.0.0"))
});
}

如果 Bevy 用这个机制——System 会变成:

// 假设 Bevy 加 capability 机制
fn my_system(rules: Capability<IGameRules>) {
// 编译期检查 CapabilityKey 存在
// 运行时检查 capability 提供者存在
}

好处:dependency declaration 是显式的,不再是隐式的”全局 Resource 顺序”。

5.2 借鉴 #2:Per-World Lockfile

Bevy 没有”存档”概念(它是引擎,不是游戏)。但如果类比:

Bevy 的 world 是临时的——关游戏就没了。保存/加载存档需要 mod 自己实现。

magic-game-harness-unity 通过 Lockfile 让”加载哪个 mod 集合”变成可重现的元数据——bevy 没有这个概念。

未来 Bevy 应用场景:一个用 Bevy 做的沙盒游戏,如果想加 mod 系统,可以借用 Lockfile 思路。

5.3 借鉴 #3:Logical Unload

Bevy 没有”卸载 plugin”概念——plugin 一旦加进 App 就一直在。

magic-game-harness-unity 通过 LIFO effect 释放让 mod 可以禁用、卸载、替换。

未来 Bevy 应用场景:如果 Bevy 加 plugin 系统,可以让 plugin 可热插拔。


6. magic-game-harness-unity 可以向 Bevy 借鉴什么

6.1 借鉴 #1:System Schedule 显式化

Bevy 的 schedule 是一等公民:

app.add_systems(Update, (
(system_a, system_b).chain(), // 顺序执行
(system_c, system_d).in_set(MySet),
).run_if(condition));

magic-game-harness-unity 当前没有 mod 内部的调度器——mod 激活后没有”每帧执行什么”的标准化接口。

未来可以加:

public interface IModuleSystem
{
void OnUpdate(UpdateContext ctx, float deltaTime);
}
ctx.RegisterSystem<MyModSystem>(new MyModSystem());
// framework 每帧调 MyModSystem.OnUpdate

6.2 借鉴 #2:Component 化游戏对象

Bevy 的”Entity = Component 集合”是革命性的——比 OOP 更灵活。

magic-game-harness-unity 当前 Mod 用的是 C# 常规类——没有 component composition。

未来可以加(spec 第 11.3 节命名空间有 Content):

public class PlayerComponent : IComponent { public int Health; }
public class EnemyComponent : IComponent { public int Damage; }
// 一个 Entity = 多个 Component
world.Spawn(new PlayerComponent { Health = 100 }, new EnemyComponent { Damage = 10 });

但这会偏离 spec 第 1 节”framework deliverable intentionally contains no genre implementation”——component composition 是 gameplay 概念,应该让 mod 实现。

6.3 借鉴 #3:Change Detection

Bevy 的 change detection 让 system 只在 component 变化时执行:

fn check_health(q: Query<&Health, Changed<Health>>) {
for h in &q {
// 只在 Health 变化时执行
}
}

magic-game-harness-unity 当前没有 change detection——mod 自己管理状态。


7. 详细对照表

维度Bevymagic-game-harness-unity评价
类型系统Rust 强类型C# 强类型平手
性能⭐⭐⭐⭐⭐ native + data-oriented⭐⭐⭐ 虚调用 + capability lookupBevy 更好
热更新❌✅magic 更好
mod 隔离没有”mod”概念✅ full lifecyclemagic 更好
类型安全编译期部分编译期 + 部分运行期验证Bevy 更好
学习曲线Rust 陡 + ECS 陡C# 缓 + capability 中magic 更好
生态成熟度✅ 成熟社区,大量第三方 plugin(crates.io 上有专门的 bevy_* 生态)0Bevy 更好
平台支持全平台Unity 平台Bevy 更好
网络同步❌(自己实现)✅ 计划中 AOT 桥magic 更好
多实例多 World多 Session平手
性能调试Tracy integrationUnity ProfilerBevy 更好(轻量)
代码体积Rust 编译大IL2CPP 编译小平手
启动时间取决于 plugin 数取决于 mod 数 + IL2CPP平手
作者生态✅ Bevy Asset Store 风格计划中 Mod SDKBevy 更好(已有)

8. Bevy 与 magic-game-harness-unity 的根本差异

维度Bevymagic-game-harness-unity
目标提供引擎(给开发者做游戏)提供框架 + mod 平台(给作者做 mod)
用户游戏开发者mod 作者 + 游戏开发者
核心 APIECS(数据+系统)IModule(生命周期 + capability)
可扩展性编译时(Rust trait)运行时(HybridCLR + capability)
作者门槛Rust + ECSC# + Unity + capability
性能取向极致(data-oriented)灵活(contract-oriented)
跨游戏一个引擎多游戏一个框架多游戏(同 spec 目标)

Bevy 是”通用引擎”——任何游戏都能用 magic-game-harness-unity 是”mod-first 框架”——任何使用它的游戏都能被 mod

两者解决不同问题——不是直接竞争关系。


9. 如果让 Bevy 设计 magic-game-harness-unity

假设 Bevy 团队来设计这个框架,会怎么做:

设计Bevy 风格magic-game-harness-unity 当前
Plugin 入口impl Pluginimpl IModule
状态机#[derive(States)] enum手工 SessionState enum
资源注册app.insert_resource(...)builder.RegisterInstance(...)
依赖注入Res<T>VContainer constructor
事件总线EventWriter<T> / EventReader<T>DiagnosticRouter + sinks
Scheduleapp.add_systems(Stage, sys1, sys2)暂无
BundleBundle trait❌

Bevy 风格的 magic-game-harness-unity会:

  • 用 Rust trait 而不是 C# interface
  • 用 ECS 而不是 capability(不可热更)
  • 用 plugin 编译期模式而不是运行时 HybridCLR
  • 用 system schedule 显式化 mod 内部调度

magic-game-harness-unity 风格的 Bevy会:

  • 加 capability key(替代全局 Resource)
  • 加 plugin unload(spec 6.6 节)
  • 加 schema migration(mod 升级不破坏存档)
  • 加 network sync(NGO 桥)

两者互补——Bevy 学 magic-game-harness-unity 的 mod 系统,magic-game-harness-unity 学 Bevy 的 ECS。


10. 案例对比

10.1 案例:做一个”移动玩家”的功能

Bevy 实现

// 1. 定义 Component
#[derive(Component)]
struct Player;
#[derive(Component)]
struct Speed(f32);
// 2. 注册 system
app.add_systems(Update, move_player);
fn move_player(
time: Res<Time>,
mut query: Query<&mut Transform, (With<Player>, With<Speed>)>,
) {
for mut transform in &mut query {
transform.translation.x += 1.0 * time.delta_seconds();
}
}
// 3. 创建玩家
fn setup(mut commands: Commands) {
commands.spawn((
Player,
Speed(5.0),
Transform::default(),
));
}

magic-game-harness-unity 实现

// 1. 定义 capability
public interface IPlayerMovement
{
void MovePlayer(SessionId session, Vector3 delta);
}
// 2. mod 提供 capability
public sealed class PlayerMovementMod : IModule
{
public ModuleDescriptor Descriptor => new ModuleDescriptor(
...,
providedCapabilities: new[]
{
new CapabilityProvision("game.player.movement", CapabilityVersion.Parse("1.0.0"))
});
public Task<ModuleOperationResult> ActivateAsync(IModuleContext ctx, CancellationToken ct)
{
ctx.RegisterCapability<IPlayerMovement>(new PlayerMovementImpl());
return Task.FromResult(ModuleOperationResult.Success());
}
public Task<ModuleOperationResult> DeactivateAsync(CancellationToken ct)
{
return Task.FromResult(ModuleOperationResult.Success());
}
}
// 3. 实现 capability
public sealed class PlayerMovementImpl : IPlayerMovement
{
public void MovePlayer(SessionId session, Vector3 delta)
{
// 调用 framework 的内部 API
var worldService = VContainer.Resolve<IWorldService>(); // 假设
worldService.TranslatePlayer(session, delta);
}
}

对比:

维度Bevymagic-game-harness-unity
数据Transform Component由 capability 实现自己管理
逻辑move_player system 函数PlayerMovementImpl.MovePlayer 方法
调用方system 自动每帧调用其他 mod 通过 ctx.GetCapability<IPlayerMovement>() 调用
可热更❌✅
可替换❌(系统是全局单例)✅(换 capability 实现)

10.2 教训:magic-game-harness-unity 的”灵活性代价”

Bevy 玩家移动 = 1 个 system + 1 个 Component magic-game-harness-unity 玩家移动 = 1 个 IModule + 1 个 contract interface + 1 个 capability registration + 调用方 GetCapability + 虚调用

代码量更大——但换来:

  • ✅ 可以热更
  • ✅ 可以替换 mod 实现
  • ✅ 可以测试(mock IPlayerMovement)

Bevy 的代码量小但耦合到 system 全局——magic-game-harness-unity 的代码量多但解耦到 capability 接口。


11. 关键 takeaway

读完对比,最大的认知收获:

Bevy 是”编译时 + 极致性能”的引擎哲学 magic-game-harness-unity 是”运行时 + 极致灵活”的框架哲学

两者解决不同问题:

  • Bevy:怎么让游戏跑得快
  • magic-game-harness-unity:怎么让游戏被 mod 替换而不崩

具体到这个项目:

  • ✅ 从 Bevy 学到了:ECS 的 component 思想可以借鉴(spec 第 11.3 节 Content 命名空间)
  • ✅ 从 Bevy 学到了:schedule 显式化让 system 调度可控
  • ❌ 不能从 Bevy 学的:data-oriented 性能优化(与 contract-oriented 灵活性冲突)
  • ❌ 不能从 Bevy 学的:编译期类型严格(mod 是运行时加载,编译期不知道所有类型)

最优解:magic-game-harness-unity 应该保留 Bevy 的 ECS 思想(Component 化、Schedule 化),但不学 Bevy 的实现(Rust trait、编译时 binding)。


12. 我对 magic-game-harness-unity 设计的评价

通过对比 Bevy,我确认了一些 spec 决策的价值,也发现了一些潜在改进点:

12.1 spec 决策 #1:Capability 而非 Resource

价值:解决 Bevy 没有的”mod 替换”问题。

12.2 spec 决策 #2:HybridCLR 而非 dynamic library

价值:解决 Bevy 没有的”运行时 mod”问题。

12.3 spec 决策 #3:Session Lifecycle State Machine

价值:解决 Bevy 没有的”plugin 失败时清理”问题。

12.4 潜在改进点 #1:缺少 Schedule 显式化

当前:mod 激活后没有”每帧执行什么”的标准化接口。

改进:引入 IModuleSystem 接口,让 mod 声明 system 函数:

public interface IModuleSystem
{
SystemStage Stage { get; } // Startup / Update / FixedUpdate / ...
void OnUpdate(SystemContext ctx, float deltaTime);
}

12.5 潜在改进点 #2:缺少 Component Composition

当前:mod 用普通 C# 类,没有 component 化。

改进:spec 第 11.3 节 Content 命名空间可以加 IComponent 接口,但框架不强制(让 mod 自己选择)。

12.6 潜在改进点 #3:缺少 Change Detection

当前:mod 自己管理状态变化。

改进:未来可以加类似 Bevy 的 Changed<T> 接口,让 mod 只在变化时执行。


13. 总结

维度Bevymagic-game-harness-unity
成熟度✅ 5.x 稳定⏳ spec + 脚手架
性能⭐⭐⭐⭐⭐⭐⭐⭐
灵活性⭐⭐⭐⭐⭐⭐⭐
类型安全⭐⭐⭐⭐⭐⭐⭐⭐⭐
生态成熟的第三方 plugin 生态0
学习曲线陡中
热更❌✅
mod 系统❌✅
跨平台⭐⭐⭐⭐⭐Unity 平台
作者生态✅计划中

最终判断:

  • 如果你想做极致性能的游戏引擎 → Bevy 风格(data-oriented)
  • 如果你想做mod-first 游戏平台 → magic-game-harness-unity 风格(contract-oriented)
  • 如果你想两全 → 等 magic-game-harness-unity 实现 Context Runtime 后看效果

参考链接


系列续作 #2。这两篇对比分析笔记(07-SMAPI、08-Bevy ECS)从两个不同维度梳理了 magic-game-harness-unity 的设计选择:

  • SMAPI = “现实的 mod 平台怎么做”
  • Bevy ECS = “理论上的游戏架构怎么做”

这两个参照系让 spec 的设计选择可被验证、可被质疑、可被借鉴。后续 Context Runtime 实现时,可以再加一篇专门讲实际的解析算法(10-Context-Runtime-Implementation.md)。


附:本篇的勘误

本篇对 harness 一侧的论证是站得住的;审查发现的问题集中在 Bevy 一侧与 API 推测上,已全部就地修正:

#原文实际
1头部写 “5.x 版本”、未声明代码所依据的版本Bevy 仍在 0.x,不存在 5.x;且每个 minor 都可能 breaking。已标注”以 0.15 为准”并说明 0.16 的 send→write 改名
2对照表 “服务定位:Query<T>”Query<T> 是按组件筛选实体,不是服务定位。取全局服务是 Res<T> / ResMut<T>,无约束访问是 &mut World
3对照表 “多实例:多个 World ↔ 多个 Session”harness 的 SessionFactory 强制同一时刻至多一个 active Session,是顺序复用而非并存
4ctx.GetCapability<T>(key)泛型方法,与 Module_context_is_not_an_unrestricted_service_locator 冲突。已加全篇代码约定说明其正确形态
5fn move_player(query: Query<&mut Transform, ...>)要写 mut query,否则不能可变借出
6Plugin::build → Plugin::ready → ...补全为 build → ready → finish → cleanup 四个钩子

完整证据见 00-Review-Report.md §2.5、§6.2。

其中第 2、3 条修正后,对比反而更有意思了:

  • 第 2 条拆开之后暴露出一个原表遮蔽了的对比——Bevy 提供 &mut World(无约束访问整个世界),harness 刻意不提供任何等价物。这不是能力强弱之分,而是信任模型之分:Bevy 的插件编译期静态链接、与引擎版本强绑定,作者就是”自己人”;harness 的模块是运行时加载的第三方二进制,给出 &mut World 等价物就等于放弃全部不变量。
  • 第 3 条则说明 harness 在”多实例”这一维上比 Bevy 弱得多,而且是刻意的:SessionFactory 的单例约束换来的是”一个 App root 一条确定的清理链”。这个取舍值得在 §12 的评价里补一句——它是简化,也是限制。
显示设置